﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Assignment_3.GenericStructures;
using Assignment_3.Scene;
using System.Threading.Tasks;
using Assignment_3.Scene.Basic_Shapes;

namespace Assignment_3.Raytracer
{
    public class Raytracer
    {
        public Ray[,] rays {get; set;}
        public Vector normal { get; set; }
        public Vector screenPosition { get; set; }
        //protected ColorVector[,] display { public get; set; }
        public int maxDepth { get; set; }
        public int mostIterationsPerformed { get; set; }
        public SceneComposition scene {get; set;}
        int width;
        int height;

        public Raytracer(int width, int height, SceneComposition sceneToRender,int maxDepth = 10)
        {
            this.width = width;
            this.height = height;
            double ratio = Math.Max((double)sceneToRender.sceneWidth / (double)this.width,(double)sceneToRender.sceneHeight / (double)this.height);
            //create array of rays of size width, height
            this.rays= new Ray[width,height];
            
            //create array of colours of size width, height
            //this.display = new ColorVector[width, height];
            //set projection plane location to 0 and direction to be z;
            this.normal = new Vector();
            this.normal.x = 0;
            this.normal.y = 0;
            this.normal.z = 1;

            this.screenPosition = new Vector();
            screenPosition.x = 0;
            screenPosition.y = 0;
            screenPosition.z = 0;
            for (int j = 0; j < height; ++j)
            {
                for (int i = 0; i < width; ++i)
                {
                    Vector rayOrigin = new Vector();
                    rayOrigin.z = 0;
                    rayOrigin.x = (width - (width / 2) - i)*ratio;
                    rayOrigin.y = (height - (height / 2) - j)*ratio;
                    this.rays[i, j] = new Ray(rayOrigin, normal);
                    this.rays[i, j].color = new ColorVector();
                }
            }
            if (sceneToRender == null)
            {
                this.scene = new SceneComposition();
            }
            else
            {
                this.scene = sceneToRender;
            }

            this.maxDepth = maxDepth;
            
        }

        public Raytracer(int width, int height, Vector normal, Vector CenterPosition, SceneComposition sceneToRender = null, int maxDepth = 10)
        {
            //create array of rays of size width, height
            this.rays = new Ray[width, height];
            //create array of colours of size width, height
            //this.display = new ColorVector[width, height];

            this.normal = normal;

            this.screenPosition = CenterPosition;

            scene = new SceneComposition();

            if (sceneToRender == null)
            {
                this.scene = new SceneComposition();
            }
            else
            {
                this.scene = sceneToRender;
            }

            this.maxDepth = maxDepth;
        }

        public void addShape(Scene.Basic_Shapes.Shape shapeToAdd)
        {
            scene.addShape(shapeToAdd);
        }

        

        public void run(int supersampling = 4)
        {
            // when this is called, run the raytracer
            double rayContribution = 1 / (double)supersampling;
            double samplespace = 1 / (Math.Sqrt(supersampling));

            Parallel.For(0,this.width, j=>
                {
                    for (int i = 0; i < this.height; i++ )
                    {
                        Ray r = rays[j, i];
                        //trace ray

                        for (double k = 0; k < 1; k += samplespace)
                        {
                            for (double l = 0; l < 1; l += samplespace)
                            {
                                Vector d = new Vector(r.origin.x, r.origin.y, r.origin.z);
                                d.x += k;
                                d.y += l;

                                Ray traced = traceRay(new Ray(d, r.direction, r.color));
                                if (traced != null)
                                {
                                    rays[j, i] += traced*rayContribution;
                                }
                            }
                        }
                    }
                }
            );
            
        }

        protected Ray traceRay(Ray r, int depth = 0 )
        {
            if (depth > this.mostIterationsPerformed)
            {
                this.mostIterationsPerformed = depth;
            }

            Ray toReturn = null; 
            Vector intersection = null;
            Assignment_3.Scene.Basic_Shapes.Shape nearestShape = null;

            getNearestIntersection(r, ref intersection, ref nearestShape);

            Ray reflection = null;
            Ray refraction = null;

            if (intersection != null)
            {
                toReturn = new Ray(r.origin, r.direction);
                if (depth < this.maxDepth)
                {
                    reflection = nearestShape.getReflectedRay(r, intersection);
                }
                if (reflection != null)
                {
                    reflection = traceRay(reflection, depth + 1);
                    
                }
                if (depth < this.maxDepth)
                {
                    refraction = nearestShape.getRefractedRay(r, intersection);
                }
                if (refraction != null)
                {
                    refraction = traceRay(refraction, depth + 1);
                }

                ColorVector pointColor = new ColorVector(0, 0, 0);

                Ray reflectedDirection = nearestShape.getReflectedRay(r, intersection);

                if (reflectedDirection != null)
                {
                    pointColor = computePointColor(intersection,nearestShape.calculateNormal(intersection,r),reflectedDirection.direction,r.origin,nearestShape);
                }

                ColorVector reflectionColor = null;
                ColorVector refractionColor = null;
                if (reflection != null)
                {
                    double reflectionCoefficient = (nearestShape.properties.reflectionCoefficient);
                    reflectionColor = reflection.color * reflectionCoefficient;
                }
                if (refraction != null)
                {
                    refractionColor = refraction.color*(1-nearestShape.properties.transparencyCoefficient);
                }

                //combine results from reflection, refraction and point colour.

                toReturn.color = pointColor*(nearestShape.properties.transparencyCoefficient);

                toReturn.color *= (1- nearestShape.properties.reflectionCoefficient);
                if (refractionColor != null)
                {
                    toReturn.color += refractionColor * (1 - nearestShape.properties.transparencyCoefficient);
                }
                if(reflectionColor != null)
                {
                    toReturn.color += reflectionColor*nearestShape.properties.transparencyCoefficient;
                }

            }

            return toReturn;
        }

        public void getNearestIntersection(Ray r, ref Vector intersection, ref Assignment_3.Scene.Basic_Shapes.Shape nearestShape)
        {
            foreach (Assignment_3.Scene.Basic_Shapes.Shape shape in this.scene.shapes)
            {
                if (intersection == null)
                {
                    Vector testItersection = shape.getFirstIntersection(r);
                    if (testItersection != null && (testItersection - r.origin).Length() > 0)
                    {
                        intersection = testItersection;
                        if (intersection != null)
                        {
                            nearestShape = shape;
                        }
                    }
                }
                else
                {
                    Vector testItersection = shape.getFirstIntersection(r);
                    if (testItersection != null && (testItersection - r.origin).Length() > 0)
                    {
                        if ((Math.Abs((testItersection - r.origin).Length()) < Math.Abs((intersection - r.origin).Length())))
                        {
                            nearestShape = shape;
                            intersection = testItersection;
                        }
                    }

                }
            }
        }

        protected ColorVector computePointColor(Vector intersection, Vector normal, Vector reflection, Vector viewPoint, Shape shape)
        {
            //double shade = 0;

            Vector directionToView = (viewPoint - intersection)/(viewPoint - intersection).Length();

            List<ColorVector> colors = new List<ColorVector>();

            ColorVector finalColor = new ColorVector(0, 0, 0); ;

            foreach (PointLight l in scene.lights)
            {
                Vector lightVector = l.origin - intersection;
                lightVector /= lightVector.Length();
                //check that point not in a shadow
                Vector intersectionPoint = null;
                Assignment_3.Scene.Basic_Shapes.Shape nearestShape = null;

                Ray r = new Ray(intersection + lightVector, lightVector);
                r.color = new ColorVector(0, 0, 0);
                getNearestIntersection(r, ref intersectionPoint, ref nearestShape);
                ColorVector diffuseColor = new ColorVector(0, 0, 0);
                ColorVector specularColor = new ColorVector(0, 0, 0);
                if(nearestShape == null)
                {
                    //calculate diffuse color
                    
                    Double diffuseShade = Vector.DotProduct(lightVector, normal);
                    if (diffuseShade < 0)
                    {
                        diffuseShade = 0;
                    }
                    diffuseColor = diffuseShade * l.color * shape.properties.coefficientDiffuseReflection;

                    //calculate specular color
                    
                    Double specularShade = Vector.DotProduct(reflection,directionToView);
                    if (specularShade < 0)
                    {

                        specularShade = 0;
                    }
                    specularShade = Math.Pow(specularShade, shape.properties.shininessCoefficient);
                    specularColor = specularShade * l.color * shape.properties.coefficientDiffuseReflection;


                }
                else if (nearestShape.properties.transparencyCoefficient < 1)
                {
                    Ray returned = traceRay(r, this.maxDepth - 2);

                    if (returned != null)
                    {
                        //calculate diffuse color

                        Double diffuseShade = Vector.DotProduct(lightVector, normal);
                        if (diffuseShade < 0)
                        {
                            diffuseShade = 0;
                        }
                        diffuseColor = diffuseShade * returned.color * shape.properties.coefficientDiffuseReflection;

                        //calculate specular color

                        Double specularShade = Vector.DotProduct(reflection, directionToView);
                        if (specularShade < 0)
                        {
                            specularShade = 0;
                        }
                        specularShade = Math.Pow(specularShade, shape.properties.shininessCoefficient);
                        specularColor = specularShade * returned.color * shape.properties.coefficientDiffuseReflection;
                    }
                }
                ColorVector ambientColor = l.color * scene.ambientLightingCoefficient;

                ColorVector total = specularColor + diffuseColor + ambientColor;

                colors.Add(total);
             
            }
            
            //sum the light contributions
            for (int i = 0; i < colors.Count; ++i)
            {
                finalColor += colors[i];
            }

            finalColor *= shape.properties.color;  

            return finalColor;
        }      

    }
}
